package net.stuffrepos.bricknetcenter.database;

import java.util.List;
import net.stuffrepos.bricknetcenter.database.HibernateUtil.Transaction;
import net.stuffrepos.util.cache.CacheableValue;
import net.stuffrepos.util.cache.CacheableVectorValue;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Session;

/**
 *
 * @author Eduardo H. Bogoni <eduardobogoni@gmail.com>
 */
public final class QueryLazyResultSet<T> implements LazyResultSet<T> {

    private static final Log log = LogFactory.getLog(QueryLazyResultSet.class);
    private static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
    private final QueryData query;
    private final HibernateUtil hibernateUtil;
    private final int pageSize;
    private CacheableVectorValue<Page> pages;
    private CacheableVectorValue<T> elements;

    public QueryLazyResultSet(HibernateUtil hibernateUtil, QueryData query) throws Exception {
        this.pageSize = DEFAULT_PAGE_SIZE;
        this.hibernateUtil = hibernateUtil;
        this.query = query;

        int totalElements = getTotalSize();
        int totalPages = (int) Math.ceil((double) totalElements / (double) pageSize);
        pages = new CacheableVectorValue<Page>(totalPages) {

            @Override
            protected Page calculate(int pageIndex) {
                log.trace("Page requested: " + pageIndex);
                return new Page(pageIndex);
            }
        };
        elements = new CacheableVectorValue<T>(totalElements) {

            @Override
            protected T calculate(int elementIndex) {
                log.trace("Element requested: " + elementIndex);
                Page page = getPageByElementIndex(elementIndex);
                assert page != null;
                return (T) page.getElementAt(elementIndex % pageSize);
            }
        };

    }

    @Override
    public int getTotalSize() throws Exception {
        return hibernateUtil.executeTransaction(new Transaction<Long>() {

            @Override
            public Long execute(Session session) throws Exception {
                return (Long) totalSizeQuery().hibernateQuery(session).uniqueResult();
            }
        }).intValue();
        //return pageSize;
    }

    @Override
    public T getElementAt(int index) throws Exception {
        return elements.getValue(index);
    }

    private QueryData totalSizeQuery() {
        int pos;
        String hql = this.query.getHql();

        if ((pos = StringUtils.indexOfIgnoreCase(hql, "order by")) >= 0) {
            hql = hql.substring(0, pos);
        }

        if ((pos = StringUtils.indexOfIgnoreCase(hql, "from")) >= 0) {
            hql = hql.substring(pos, hql.length());
        }

        return query.copyParameters("select count(*) " + hql);
    }

    private Page getPageByElementIndex(int elementIndex) {
        return pages.getValue(elementIndex / pageSize);
    }

    @Override
    public List<T> fetchAll() throws Exception {
        return hibernateUtil.executeTransaction(new Transaction<List<T>>() {

            @Override
            public List<T> execute(Session session) throws Exception {
                return query.hibernateQuery(session).list();
            }
        });
    }

    private class Page {

        private final int pageIndex;
        private CacheableValue<List> results = new CacheableValue<List>() {

            @Override
            protected List calculate() {
                try {
                    return hibernateUtil.executeTransaction(new Transaction<List>() {

                        @Override
                        public List execute(Session session) throws Exception {
                            return query.paginate(pageIndex, pageSize).hibernateQuery(session).list();
                        }
                    });
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }

            }
        };

        private Page(int pageIndex) {
            this.pageIndex = pageIndex;
        }

        public Object getElementAt(int elementIndex) {
            return results.getValue().get(elementIndex);
        }
    }
}
